#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <termios.h>

void setFileAsync(int fd, int async);
void readHandshake();
int launchShell(int argc, char **argv);
int launchTerminal(int argc, char **argv);
void printBanner();
void printHelpText();
void printVersion();
void writeString(int fd, const char *str);
#ifdef __CYGWIN__
void cfmakeraw(struct termios *termios_p);
#endif

// The frontend detects whether we're running inside guishell_term or not, by
// sending it ^E (enquiry) and seeing what it answers. If it is, sends a
// special escape sequence to guiterm to let it know that guishell_sh is
// starting, then exec()s guishell_sh. If not, exec()s guishell_term.
int main(int argc, char **argv)
{
	struct termios oldTermFlags;
	struct termios newTermFlags;
	int ii;
	int termIsGuishell = 0;
	
	// Check command-line arguments. If the '-t' option is present, skip
	// detection and launch the terminal immediately.
	for(ii=1; ii<argc; ii++)
	{
		if(!strcmp(argv[ii], "-t") || !strcmp(argv[ii], "--terminal"))
			return launchTerminal(argc, argv);
		else if(!strcmp(argv[ii], "-h") || !strcmp(argv[ii], "--help")) {
			printHelpText();
			return 0;
		} else if(!strcmp(argv[ii], "-v") || !strcmp(argv[ii], "--version")) {
			printVersion();
			return 0;
		}
	}
	
	printBanner();

	// Make stdin raw
	if(tcgetattr(0, &oldTermFlags)==0) {
		newTermFlags = oldTermFlags;
		cfmakeraw(&newTermFlags);
		tcsetattr(0, TCSANOW, &newTermFlags);
	}
	
	// Send an 'Enquiry' (\05) followed by a  'Query Device Code' (ESC [ c).
	// If the terminal is guishell, it will respond to the enquiry by sending
	// __guishell__, and to the QDC by sending an escape sequence (ESC [ ? ...)
	// If the terminal is something else, it will either respond to enquiry
	// either with its name or ignore it, and will respond to the QDC with an
	// escape sequence.
	writeString(1, "\05\033[c");
	
	// Make stdin asynchronous
	setFileAsync(0, 1);
	
	// Wait for input. We keep reading until either we see __guishell__,
	// or see ESC [ <code> 0c, or two seconds have passed.
	struct timeval startTime;
	gettimeofday(&startTime, NULL);
	char inbuf[256];
	int bytesRead = 0;
	
	while(1)
	{
		// Wait until some input is ready (or up to .1 seconds)
		fd_set readfds;
		struct timeval timeout = {0, 100000};
		int ret;
		
		FD_ZERO(&readfds);
		FD_SET(0, &readfds);
		select(1, &readfds, NULL, &readfds, &timeout);
		
		// Read any input
		ret = read(0, inbuf+bytesRead, 256-bytesRead-1);
		if(ret >= 0) bytesRead += ret;
		
		// Check for a match of '__guishell__'
		inbuf[bytesRead] = 0;
		
		if(strstr(inbuf, "__guishell__")) {
			termIsGuishell = 1;
			break;
		}
		
		// Check for a match of ESC [ ? ...
		if(strstr(inbuf, "\033[?")) {
			break;
		}
		
		// Check for timeout
		struct timeval currentTime;
		gettimeofday(&currentTime, NULL);
		struct timeval timeElapsed;
		timersub(&currentTime, &startTime, &timeElapsed);
		if(timeElapsed.tv_sec >= 2) {
			printf("Timed out. Input was (%i): { ", bytesRead); //DEBUG
			for(ii=0; ii<bytesRead; ii++) //DEBUG
				printf("%i ", (int)inbuf[ii]); //DEBUG
			printf("}\n"); //DEBUG
			break;
		}
	}
	
	if(termIsGuishell) {
		// Terminal is guishell, so finish the handshake.
		// First notify the terminal that a nested session is starting, with a
		// special escape sequence
		setFileAsync(1, 0);
		writeString(1, "\x1b]gbegin\n");
		setFileAsync(1, 1);
		
		// The terminal will respond with its own protocol start. Read one
		// character at a time until we've read everything before it, and it
		// itself, but nothing after.
		readHandshake();
		
		// Invoke the shell
		launchShell(argc, argv);
	}
	else
	{
		// Terminal is not guishell, so launch it (via X11). But first set the
		// terminal settings back as they were.
		tcsetattr(0, TCSANOW, &oldTermFlags);
		printf("Terminal is not guishell\n");
		launchTerminal(argc, argv);
	}
}

void readHandshake()
{
	char inbuf[256];
	int bytesRead = 0;
	
	setFileAsync(0, 0);
	
	while(1)
	{
		// Read only one byte at a time (because we need to leave anything
		// after the thing we're looking for in the pipe, so guishell_sh can
		// read it.)
		int ret = 0;
		ret = read(0, inbuf+bytesRead, 1);
		if(ret == 0) {
			// Unexpected end of file. But the terminal has already been
			// converted into a stream for the shell to use, so we can't
			// display an error message there, so just abort.
			exit(1);
		}
		else if(ret >= 0)
			bytesRead += ret;
		
		inbuf[bytesRead] = 0;
		
		if(bytesRead >= 255)
		{
			// Shouldn't read this much without seeing a handshake; something's
			// wrong, so abort.
			exit(1);
		}
		
		if(strstr(inbuf, "\x1b]gbegin\n"))
			break;
	}
}

int launchShell(int argc, char **argv)
{
	// TODO: Path handling
	execv("./guishell_sh", argv);
	fprintf(stderr, "Failed to run guishell_sh: %s.\n", strerror(errno));
	exit(0);
}

int launchTerminal(int argc, char **argv)
{
	// TODO: Path handling
	printf("Wrong terminal type; launching new terminal.\n");
	execv("./guishell_term", argv);
	fprintf(stderr, "Failed to run guishell_term: %s.\n", strerror(errno));
	exit(0);
}

void printBanner()
{
	printf("Checking terminal type...\n");
}

void printHelpText()
{
	// TODO
	printf("Guishell help text\n");
}

void printVersion()
{
	// TODO
	printf("Guishell version string\n");
}

//////////////////////////////////////////////////////////////////////////////

void setFileAsync(int fd, int async)
{
	int oldFlags;
	oldFlags = fcntl(fd, F_GETFL);
	
	if(async)
		fcntl(fd, F_SETFL, oldFlags|O_NONBLOCK);
	else
		fcntl(fd, F_SETFL, oldFlags&~O_NONBLOCK);
}

#ifdef __CYGWIN__
// Workaround for Cygwin, which is missing cfmakeraw. Pasted from man page.
void cfmakeraw(struct termios *termios_p)
{
    termios_p->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
    termios_p->c_oflag &= ~OPOST;
    termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
    termios_p->c_cflag &= ~(CSIZE|PARENB);
    termios_p->c_cflag |= CS8;
}
#endif

void writeString(int fd, const char *str)
{
	int len = strlen(str);
	
	while(len > 0)
	{
		int bytesWritten;
		
		bytesWritten = write(fd, str, len);
		if(bytesWritten > 0) {
			str += bytesWritten;
			len -= bytesWritten;
		} else if(bytesWritten < 0) {
			sleep(0);
		}
	}
}

